home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Resources / Browsers, Managers & Extensions / Mozilla Weave 0.2.7 / latest-weave.xpi / modules / remote.js < prev    next >
Text File  |  2008-08-08  |  24KB  |  725 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Bookmarks Sync.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Dan Mills <thunder@mozilla.com>
  22.  *
  23.  * Alternatively, the contents of this file may be used under the terms of
  24.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  25.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26.  * in which case the provisions of the GPL or the LGPL are applicable instead
  27.  * of those above. If you wish to allow use of your version of this file only
  28.  * under the terms of either the GPL or the LGPL, and not to allow others to
  29.  * use your version of this file under the terms of the MPL, indicate your
  30.  * decision by deleting the provisions above and replace them with the notice
  31.  * and other provisions required by the GPL or the LGPL. If you do not delete
  32.  * the provisions above, a recipient may use your version of this file under
  33.  * the terms of any one of the MPL, the GPL or the LGPL.
  34.  *
  35.  * ***** END LICENSE BLOCK ***** */
  36.  
  37. const EXPORTED_SYMBOLS = ['Resource', 'JsonFilter', 'CryptoFilter',
  38.                           'Keychain', 'RemoteStore'];
  39.  
  40. const Cc = Components.classes;
  41. const Ci = Components.interfaces;
  42. const Cr = Components.results;
  43. const Cu = Components.utils;
  44.  
  45. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  46. Cu.import("resource://weave/log4moz.js");
  47. Cu.import("resource://weave/constants.js");
  48. Cu.import("resource://weave/util.js");
  49. Cu.import("resource://weave/crypto.js");
  50. Cu.import("resource://weave/async.js");
  51. Cu.import("resource://weave/identity.js");
  52. Cu.import("resource://weave/dav.js");
  53. Cu.import("resource://weave/stores.js");
  54.  
  55. Function.prototype.async = Async.sugar;
  56.  
  57.  
  58. function RequestException(resource, action, request) {
  59.   this._resource = resource;
  60.   this._action = action;
  61.   this._request = request;
  62.   this.location = Components.stack.caller;
  63. }
  64. RequestException.prototype = {
  65.   get resource() { return this._resource; },
  66.   get action() { return this._action; },
  67.   get request() { return this._request; },
  68.   get status() { return this._request.status; },
  69.   toString: function ReqEx_toString() {
  70.     return "Could not " + this._action + " resource " + this._resource.path +
  71.       " (" + this._request.status + ")";
  72.   }
  73. };
  74.  
  75. function Resource(path) {
  76.   this._init(path);
  77. }
  78. Resource.prototype = {
  79.   get identity() { return this._identity; },
  80.   set identity(value) { this._identity = value; },
  81.  
  82.   get dav() { return this._dav; },
  83.   set dav(value) { this._dav = value; },
  84.  
  85.   get path() { return this._path; },
  86.   set path(value) {
  87.     this._dirty = true;
  88.     this._path = value;
  89.   },
  90.  
  91.   get data() { return this._data; },
  92.   set data(value) {
  93.     this._dirty = true;
  94.     this._data = value;
  95.   },
  96.  
  97.   __os: null,
  98.   get _os() {
  99.     if (!this.__os)
  100.       this.__os = Cc["@mozilla.org/observer-service;1"]
  101.         .getService(Ci.nsIObserverService);
  102.     return this.__os;
  103.   },
  104.  
  105.   get lastRequest() { return this._lastRequest; },
  106.   get downloaded() { return this._downloaded; },
  107.   get dirty() { return this._dirty; },
  108.  
  109.   pushFilter: function Res_pushFilter(filter) {
  110.     this._filters.push(filter);
  111.   },
  112.  
  113.   popFilter: function Res_popFilter() {
  114.     return this._filters.pop();
  115.   },
  116.  
  117.   clearFilters: function Res_clearFilters() {
  118.     this._filters = [];
  119.   },
  120.  
  121.   _init: function Res__init(path) {
  122.     this._identity = null; // unused
  123.     this._dav = null; // unused
  124.     this._path = path;
  125.     this._data = null;
  126.     this._downloaded = false;
  127.     this._dirty = false;
  128.     this._filters = [];
  129.     this._lastRequest = null;
  130.     this._log = Log4Moz.Service.getLogger("Service.Resource");
  131.   },
  132.  
  133.   // note: this is unused, and it's not clear whether it's useful or not
  134.   _sync: function Res__sync() {
  135.     let self = yield;
  136.     let ret;
  137.  
  138.     // If we've set the locally stored value, upload it.  If we
  139.     // haven't, and we haven't yet downloaded this resource, then get
  140.     // it.  Otherwise do nothing (don't try to get it every time)
  141.  
  142.     if (this.dirty) {
  143.       this.put(self.cb);
  144.       ret = yield;
  145.  
  146.     } else if (!this.downloaded) {
  147.       this.get(self.cb);
  148.       ret = yield;
  149.     }
  150.  
  151.     self.done(ret);
  152.   },
  153.   sync: function Res_sync(onComplete) {
  154.     this._sync.async(this, onComplete);
  155.   },
  156.  
  157.   _request: function Res__request(action, data) {
  158.     let self = yield;
  159.     let listener, timer;
  160.     let iter = 0;
  161.  
  162.     if ("PUT" == action) {
  163.       for each (let filter in this._filters) {
  164.         data = yield filter.beforePUT.async(filter, self.cb, data);
  165.       }
  166.     }
  167.  
  168.     while (true) {
  169.       switch (action) {
  170.       case "GET":
  171.         DAV.GET(this.path, self.cb);
  172.         break;
  173.       case "PUT":
  174.         DAV.PUT(this.path, data, self.cb);
  175.         break;
  176.       case "DELETE":
  177.         DAV.DELETE(this.path, self.cb);
  178.         break;
  179.       default:
  180.         throw "Unknown request action for Resource";
  181.       }
  182.       this._lastRequest = yield;
  183.  
  184.       if (action == "DELETE" &&
  185.           Utils.checkStatus(this._lastRequest.status, null, [[200,300],404])) {
  186.         this._dirty = false;
  187.         this._data = null;
  188.         break;
  189.  
  190.       } else if (Utils.checkStatus(this._lastRequest.status)) {
  191.         this._log.debug(action + " request successful");
  192.         this._dirty = false;
  193.         if (action == "GET")
  194.           this._data = this._lastRequest.responseText;
  195.         //else if (action == "PUT")
  196.         //  this._data = data; // wrong! (because of filters)
  197.         break;
  198.  
  199.       } else if (action == "GET" && this._lastRequest.status == 404) {
  200.         throw new RequestException(this, action, this._lastRequest);
  201.  
  202.       } else if (iter >= 10) {
  203.         // iter too big? bail
  204.         throw new RequestException(this, action, this._lastRequest);
  205.  
  206.       } else {
  207.         // wait for a bit and try again
  208.         if (!timer) {
  209.           listener = new Utils.EventListener(self.cb);
  210.           timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  211.         }
  212.         yield timer.initWithCallback(listener, iter * iter * 1000,
  213.                                      timer.TYPE_ONE_SHOT);
  214.         iter++;
  215.       }
  216.     }
  217.  
  218.     if ("GET" == action) {
  219.       let filters = this._filters.slice(); // reverse() mutates, so we copy
  220.       for each (let filter in filters.reverse()) {
  221.         this._data = yield filter.afterGET.async(filter, self.cb, this._data);
  222.       }
  223.     }
  224.  
  225.     self.done(this._data);
  226.   },
  227.  
  228.   get: function Res_get(onComplete) {
  229.     this._request.async(this, onComplete, "GET");
  230.   },
  231.  
  232.   put: function Res_put(onComplete, data) {
  233.     if ("undefined" == typeof(data))
  234.       data = this._data;
  235.     this._request.async(this, onComplete, "PUT", data);
  236.   },
  237.  
  238.   delete: function Res_delete(onComplete) {
  239.     this._request.async(this, onComplete, "DELETE");
  240.   }
  241. };
  242.  
  243. function ResourceSet(basePath) {
  244.   this._init(basePath);
  245. }
  246. ResourceSet.prototype = {
  247.   __proto__: new Resource(),
  248.   _init: function ResSet__init(basePath) {
  249.     this.__proto__.__proto__._init.call(this);
  250.     this._basePath = basePath;
  251.     this._log = Log4Moz.Service.getLogger("Service.ResourceSet");
  252.   },
  253.  
  254.   _hack: function ResSet__hack(action, id, data) {
  255.     let self = yield;
  256.     let savedData = this._data;
  257.  
  258.     if ("PUT" == action)
  259.       this._data = data;
  260.  
  261.     this._path = this._basePath + id;
  262.     yield this._request.async(this, self.cb, action, data);
  263.  
  264.     let newData = this._data;
  265.     this._data = savedData;
  266.     if (this._data == null)
  267.       this._data = {};
  268.     this._data[id] = newData;
  269.  
  270.     self.done(this._data[id]);
  271.   },
  272.  
  273.   get: function ResSet_get(onComplete, id) {
  274.     this._hack.async(this, onComplete, "GET", id);
  275.   },
  276.  
  277.   put: function ResSet_put(onComplete, id, data) {
  278.     this._hack.async(this, onComplete, "PUT", id, data);
  279.   },
  280.  
  281.   delete: function ResSet_delete(onComplete, id) {
  282.     this._hack.async(this, onComplete, "DELETE", id);
  283.   }
  284. };
  285.  
  286. function ResourceFilter() {
  287.   this._log = Log4Moz.Service.getLogger("Service.ResourceFilter");
  288. }
  289. ResourceFilter.prototype = {
  290.   beforePUT: function ResFilter_beforePUT(data) {
  291.     let self = yield;
  292.     this._log.debug("Doing absolutely nothing")
  293.     self.done(data);
  294.   },
  295.   afterGET: function ResFilter_afterGET(data) {
  296.     let self = yield;
  297.     this._log.debug("Doing absolutely nothing")
  298.     self.done(data);
  299.   }
  300. };
  301.  
  302. function JsonFilter() {
  303.   this._log = Log4Moz.Service.getLogger("Service.JsonFilter");
  304. }
  305. JsonFilter.prototype = {
  306.   __proto__: new ResourceFilter(),
  307.  
  308.   __os: null,
  309.   get _os() {
  310.     if (!this.__os)
  311.       this.__os = Cc["@mozilla.org/observer-service;1"]
  312.         .getService(Ci.nsIObserverService);
  313.     return this.__os;
  314.   },
  315.  
  316.   get _json() {
  317.     let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  318.     this.__defineGetter__("_json", function() json);
  319.     return this._json;
  320.   },
  321.  
  322.   beforePUT: function JsonFilter_beforePUT(data) {
  323.     let self = yield;
  324.     this._log.debug("Encoding data as JSON");
  325.     this._os.notifyObservers(null, "weave:service:sync:status", "stats.encoding-json");
  326.     self.done(this._json.encode(data));
  327.   },
  328.  
  329.   afterGET: function JsonFilter_afterGET(data) {
  330.     let self = yield;
  331.     this._log.debug("Decoding JSON data");
  332.     this._os.notifyObservers(null, "weave:service:sync:status", "stats.decoding-json");
  333.     self.done(this._json.decode(data));
  334.   }
  335. };
  336.  
  337. function CryptoFilter(identity) {
  338.   this._identity = identity;
  339.   this._log = Log4Moz.Service.getLogger("Service.CryptoFilter");
  340. }
  341. CryptoFilter.prototype = {
  342.   __proto__: new ResourceFilter(),
  343.  
  344.   get _os() {
  345.     let os = Cc["@mozilla.org/observer-service;1"].
  346.       getService(Ci.nsIObserverService);
  347.     this.__defineGetter__("_os", function() os);
  348.     return os;
  349.   },
  350.  
  351.   beforePUT: function CryptoFilter_beforePUT(data) {
  352.     let self = yield;
  353.     this._log.debug("Encrypting data");
  354.     this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting");
  355.     let ret = yield Crypto.encryptData.async(Crypto, self.cb, data, this._identity);
  356.     self.done(ret);
  357.   },
  358.  
  359.   afterGET: function CryptoFilter_afterGET(data) {
  360.     let self = yield;
  361.     this._log.debug("Decrypting data");
  362.     this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting");
  363.     let ret = yield Crypto.decryptData.async(Crypto, self.cb, data, this._identity);
  364.     self.done(ret);
  365.   }
  366. };
  367.  
  368. function Keychain(prefix) {
  369.   this._init(prefix);
  370. }
  371. Keychain.prototype = {
  372.   __proto__: new Resource(),
  373.   _init: function Keychain__init(prefix) {
  374.     this.__proto__.__proto__._init.call(this, prefix + "keys.json");
  375.     this.pushFilter(new JsonFilter());
  376.   },
  377.   _initialize: function Keychain__initialize(identity) {
  378.     let self = yield;
  379.     let wrappedSymkey;
  380.  
  381.     if ("none" != Utils.prefs.getCharPref("encryption")) {
  382.       this._os.notifyObservers(null, "weave:service:sync:status", "status.generating-random-key");
  383.  
  384.       yield Crypto.randomKeyGen.async(Crypto, self.cb, identity);
  385.  
  386.       // Wrap (encrypt) this key with the user's public key.
  387.       let idRSA = ID.get('WeaveCryptoID');
  388.       this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting-key");
  389.       wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb,
  390.                                                  identity.bulkKey, idRSA);
  391.     }
  392.  
  393.     let keys = {ring: {}, bulkIV: identity.bulkIV};
  394.     this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-key");
  395.     keys.ring[identity.username] = wrappedSymkey;
  396.     yield this.put(self.cb, keys);
  397.   },
  398.   initialize: function Keychain_initialize(onComplete, identity) {
  399.     this._initialize.async(this, onComplete, identity);
  400.   },
  401.   _getKeyAndIV: function Keychain__getKeyAndIV(identity) {
  402.     let self = yield;
  403.  
  404.     this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-keyring");
  405.  
  406.     yield this.get(self.cb);
  407.  
  408.     if (!this.data || !this.data.ring || !this.data.ring[identity.username])
  409.       throw "Keyring does not contain a key for this user";
  410.  
  411.     // Unwrap (decrypt) the key with the user's private key.
  412.     this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting-key");
  413.     let idRSA = ID.get('WeaveCryptoID');
  414.     let symkey = yield Crypto.unwrapKey.async(Crypto, self.cb,
  415.                            this.data.ring[identity.username], idRSA);
  416.     let iv = this.data.bulkIV;
  417.  
  418.     identity.bulkKey = symkey;
  419.     identity.bulkIV  = iv;
  420.   },
  421.   _setKey: function KeyChain__setKey(bulkID, newID) {
  422.     /* FIXME!: It's possible that the keyring is changed on the server
  423.        after we do a GET. Then we're just uploading this new local keyring,
  424.        thereby losing any changes made on the server keyring since this GET.
  425.     
  426.        Also, if this.data was not instantiated properly (i.e. you're
  427.        using KeyChain directly instead of getting it from the engine),
  428.        you run the risk of wiping the server-side keychain.
  429.     */
  430.     let self = yield;
  431.  
  432.     this.get(self.cb);
  433.     yield;
  434.     
  435.     let wrappedKey = yield Crypto.wrapKey.async(Crypto, self.cb,
  436.                           bulkID.bulkKey, newID);
  437.     this.data.ring[newID.username] = wrappedKey;
  438.     this.put(self.cb, this.data);
  439.     yield;
  440.   },
  441.   getKeyAndIV: function Keychain_getKeyAndIV(onComplete, identity) {
  442.     this._getKeyAndIV.async(this, onComplete, identity);
  443.   },
  444.   setKey: function Keychain_setKey(onComplete, bulkID, newID) {
  445.     this._setKey.async(this, onComplete, bulkID, newID);
  446.   }
  447. };
  448.  
  449. function RemoteStore(engine) {
  450.   this._engine = engine;
  451.   this._log = Log4Moz.Service.getLogger("Service.RemoteStore");
  452. }
  453. RemoteStore.prototype = {
  454.   get serverPrefix() this._engine.serverPrefix,
  455.   get engineId() this._engine.engineId,
  456.  
  457.   __os: null,
  458.   get _os() {
  459.     if (!this.__os)
  460.       this.__os = Cc["@mozilla.org/observer-service;1"]
  461.         .getService(Ci.nsIObserverService);
  462.     return this.__os;
  463.   },
  464.  
  465.   get status() {
  466.     let status = new Resource(this.serverPrefix + "status.json");
  467.     status.pushFilter(new JsonFilter());
  468.     this.__defineGetter__("status", function() status);
  469.     return status;
  470.   },
  471.  
  472.   get keys() {
  473.     let keys = new Keychain(this.serverPrefix);
  474.     this.__defineGetter__("keys", function() keys);
  475.     return keys;
  476.   },
  477.  
  478.   get _snapshot() {
  479.     let snapshot = new Resource(this.serverPrefix + "snapshot.json");
  480.     snapshot.pushFilter(new JsonFilter());
  481.     snapshot.pushFilter(new CryptoFilter(this._engine.engineId));
  482.     this.__defineGetter__("_snapshot", function() snapshot);
  483.     return snapshot;
  484.   },
  485.  
  486.   get _deltas() {
  487.     let deltas = new ResourceSet(this.serverPrefix + "deltas/");
  488.     deltas.pushFilter(new JsonFilter());
  489.     deltas.pushFilter(new CryptoFilter(this._engine.engineId));
  490.     this.__defineGetter__("_deltas", function() deltas);
  491.     return deltas;
  492.   },
  493.  
  494.   _openSession: function RStore__openSession(lastSyncSnap) {
  495.     let self = yield;
  496.  
  497.     if (!this.serverPrefix || !this.engineId)
  498.       throw "Cannot initialize RemoteStore: engine has no server prefix or crypto ID";
  499.  
  500.     this.status.data = null;
  501.     this.keys.data = null;
  502.     this._snapshot.data = null;
  503.     this._deltas.data = null;
  504.     this._lastSyncSnap = lastSyncSnap;
  505.  
  506.     let ret = yield DAV.MKCOL(this.serverPrefix + "deltas", self.cb);
  507.     if (!ret)
  508.       throw "Could not create remote folder";
  509.  
  510.     this._log.debug("Downloading status file");
  511.     this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-status");
  512.  
  513.     yield this.status.get(self.cb);
  514.     this._log.debug("Downloading status file... done");
  515.  
  516.     // Bail out if the server has a newer format version than we can parse
  517.     if (this.status.data.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) {
  518.       this._log.error("Server uses storage format v" +
  519.                       this.status.data.formatVersion +
  520.                       ", this client understands up to v" +
  521.                       ENGINE_STORAGE_FORMAT_VERSION);
  522.       throw "Incompatible remote store format";
  523.     }
  524.  
  525.     if (this.status.data.GUID != lastSyncSnap.GUID) {
  526.       this._log.trace("Remote GUID: " + this.status.data.GUID);
  527.       this._log.trace("Local GUID: " + lastSyncSnap.GUID);
  528.       this._log.debug("Server wipe since last sync, resetting last sync snapshot");
  529.       lastSyncSnap.wipe();
  530.       lastSyncSnap.GUID = this.status.data.GUID;
  531.       // yield this._store.resetGUIDs(self.cb); // XXX not sure if this is really needed (and it needs to be done from the engine if so)
  532.     }
  533.  
  534.     this._log.info("Last sync snapshot version: " + lastSyncSnap.version);
  535.     this._log.info("Server maxVersion: " + this.status.data.maxVersion);
  536.  
  537.     if ("none" != Utils.prefs.getCharPref("encryption"))
  538.       yield this.keys.getKeyAndIV(self.cb, this.engineId);
  539.   },
  540.   openSession: function RStore_openSession(onComplete, lastSyncSnap) {
  541.     this._openSession.async(this, onComplete, lastSyncSnap);
  542.   },
  543.  
  544.   closeSession: function RStore_closeSession() {
  545.     this.status.data = null;
  546.     this.keys.data = null;
  547.     this._snapshot.data = null;
  548.     this._deltas.data = null;
  549.     this._lastSyncSnap = null;
  550.   },
  551.  
  552.   // Does a fresh upload of the given snapshot to a new store
  553.   // FIXME: add 'metadata' arg here like appendDelta's
  554.   _initialize: function RStore__initialize(snapshot) {
  555.     let self = yield;
  556.  
  557.     yield this.keys.initialize(self.cb, this.engineId);
  558.     this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot");
  559.     yield this._snapshot.put(self.cb, snapshot.data);
  560.  
  561.     let c = 0;
  562.     for (GUID in snapshot.data)
  563.       c++;
  564.  
  565.     this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status");
  566.     yield this.status.put(self.cb,
  567.                           {GUID: snapshot.GUID,
  568.                            formatVersion: ENGINE_STORAGE_FORMAT_VERSION,
  569.                            snapVersion: snapshot.version,
  570.                            maxVersion: snapshot.version,
  571.                            snapEncryption: Crypto.defaultAlgorithm,
  572.                            deltasEncryption: Crypto.defaultAlgorithm,
  573.                            itemCount: c});
  574.     this._log.info("Full upload to server successful");
  575.   },
  576.   initialize: function RStore_initialize(onComplete, snapshot) {
  577.     this._initialize.async(this, onComplete, snapshot);
  578.   },
  579.  
  580.   // Removes server files - you may want to run initialize() after this
  581.   // FIXME: might want to do a PROPFIND instead (to catch all deltas in one go)
  582.   _wipe: function Engine__wipe() {
  583.     let self = yield;
  584.     this._log.debug("Deleting remote store data");
  585.     yield this.status.delete(self.cb);
  586.     yield this.keys.delete(self.cb);
  587.     yield this._snapshot.delete(self.cb);
  588.     //yield this._deltas.delete(self.cb);
  589.     this._log.debug("Server files deleted");
  590.   },
  591.   wipe: function Engine_wipe(onComplete) {
  592.     this._wipe.async(this, onComplete)
  593.   },
  594.  
  595.   // Gets the latest server snapshot by downloading all server files
  596.   // (snapshot + deltas)
  597.   _getLatestFromScratch: function RStore__getLatestFromScratch() {
  598.     let self = yield;
  599.  
  600.     this._log.info("Downloading all server data from scratch");
  601.     this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-snapshot");
  602.  
  603.     let snap = new SnapshotStore();
  604.     snap.data = yield this._snapshot.get(self.cb);
  605.  
  606.     this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas");
  607.     let status = this.status.data;
  608.     for (let id = status.snapVersion + 1; id <= status.maxVersion; id++) {
  609.       let delta = yield this._deltas.get(self.cb, id);
  610.       yield snap.applyCommands.async(snap, self.cb, delta);
  611.     }
  612.  
  613.     self.done(snap.data);
  614.   },
  615.  
  616.   // Gets the latest server snapshot by downloading only the necessary
  617.   // deltas from the given snapshot (but may fall back to a full download)
  618.   _getLatestFromSnap: function RStore__getLatestFromSnap() {
  619.     let self = yield;
  620.     let deltas, snap = new SnapshotStore();
  621.     snap.version = this.status.data.maxVersion;
  622.  
  623.     if (!this._lastSyncSnap ||
  624.         this._lastSyncSnap.version < this.status.data.snapVersion) {
  625.       this._log.trace("Getting latest from scratch (last sync snap too old)");
  626.       snap.data = yield this._getLatestFromScratch.async(this, self.cb);
  627.       self.done(snap.data);
  628.       return;
  629.  
  630.     } else if (this._lastSyncSnap.version >= this.status.data.snapVersion &&
  631.                this._lastSyncSnap.version < this.status.data.maxVersion) {
  632.       this._log.debug("Using last sync snapshot as starting point for server snapshot");
  633.       snap.data = Utils.deepCopy(this._lastSyncSnap.data);
  634.       this._log.info("Downloading server deltas");
  635.       this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas");
  636.       deltas = [];
  637.       let min = this._lastSyncSnap.version + 1;
  638.       let max = this.status.data.maxVersion;
  639.       for (let id = min; id <= max; id++) {
  640.         let delta = yield this._deltas.get(self.cb, id);
  641.         deltas.push(delta);
  642.       }
  643.  
  644.     } else if (this._lastSyncSnap.version == this.status.data.maxVersion) {
  645.       this._log.debug("Using last sync snapshot as server snapshot (snap version == max version)");
  646.       this._log.trace("Local snapshot version == server maxVersion");
  647.       snap.data = Utils.deepCopy(this._lastSyncSnap.data);
  648.       deltas = [];
  649.  
  650.     } else { // this._lastSyncSnap.version > this.status.data.maxVersion
  651.       this._log.error("Server snapshot is older than local snapshot");
  652.       throw "Server snapshot is older than local snapshot";
  653.     }
  654.  
  655.     try {
  656.       for (var i = 0; i < deltas.length; i++) {
  657.         yield snap.applyCommands.async(snap, self.cb, deltas[i]);
  658.       }
  659.     } catch (e) {
  660.       this._log.warn("Error applying remote deltas to saved snapshot, attempting a full download");
  661.       this._log.debug("Exception: " + Utils.exceptionStr(e));
  662.       this._log.trace("Stack:\n" + Utils.stackTrace(e));
  663.       snap.data = yield this._getLatestFromScratch.async(this, self.cb);
  664.     }
  665.  
  666.     self.done(snap.data);
  667.   },
  668.  
  669.   // get the latest server snapshot.  If a snapshot is given, try to
  670.   // download only the necessary deltas to get to the latest
  671.   _wrap: function RStore__wrap() {
  672.     let self = yield;
  673.     let ret = yield this._getLatestFromSnap.async(this, self.cb);
  674.     self.done(ret);
  675.   },
  676.   wrap: function RStore_wrap(onComplete) {
  677.     this._wrap.async(this, onComplete);
  678.   },
  679.  
  680.   // Adds a new set of changes (a delta) to this store
  681.   _appendDelta: function RStore__appendDelta(snapshot, delta, metadata) {
  682.     let self = yield;
  683.  
  684.     if (metadata) {
  685.       for (let key in metadata)
  686.         this.status.data[key] = metadata[key];
  687.     }
  688.  
  689.     let c = 0;
  690.     for (item in snapshot.data)
  691.       c++;
  692.     this.status.data.itemCount = c;
  693.  
  694.     let id = ++this.status.data.maxVersion;
  695.  
  696.     // upload the delta even if we upload a new snapshot, so other clients
  697.     // can be spared of a full re-download
  698.     this._os.notifyObservers(null, "weave:service:sync:status",
  699.                              "status.uploading-deltas");
  700.     yield this._deltas.put(self.cb, id, delta);
  701.  
  702.     // if we have more than KEEP_DELTAS, then upload a new snapshot
  703.     // this allows us to remove old deltas
  704.     if ((id - this.status.data.snapVersion) > KEEP_DELTAS) {
  705.       this._os.notifyObservers(null, "weave:service:sync:status",
  706.                                "status.uploading-snapshot");
  707.       yield this._snapshot.put(self.cb, snapshot.data);
  708.       this.status.data.snapVersion = id;
  709.     }
  710.  
  711.     // XXX we could define another constant here
  712.     // (e.g. KEEP_MAX_DELTAS) to define when to actually start
  713.     // deleting deltas from the server.  However, we can do this more
  714.     // efficiently server-side
  715.  
  716.     // finally, upload a new status file
  717.     this._os.notifyObservers(null, "weave:service:sync:status",
  718.                              "status.uploading-status");
  719.     yield this.status.put(self.cb);
  720.   },
  721.   appendDelta: function RStore_appendDelta(onComplete, snapshot, delta, metadata) {
  722.     this._appendDelta.async(this, onComplete, snapshot, delta, metadata);
  723.   }
  724. };
  725.